
module Pattern

 class Property

  @@keywords ||= {}
  @@options ||= {}

  def self.enable who, keyword, options = {}
    who.class_eval <<RUBY
      def self.#{keyword} attribute, *a
        #{self.to_s}.define(self, '#{keyword}', attribute, a)
      end
RUBY

    @@options[who] ||= {}
    @@options[who][keyword.to_sym] = options
  end

  def self.define who, keyword, attribute, definition
    #keyword = keyword.to_s

    unless attribute.is_a? Symbol
      raise "attribute argument must be a Symbol, was #{attribute.inspect}"
    end

    attribute = attribute.to_sym
    keyword = keyword.to_sym

    #puts "\n\n" + attribute.inspect + " " + definition.inspect

    @@keywords[who] ||= {}
    @@keywords[who][keyword] ||= {}

    getter_found = false
    setter_found = false


    who.ancestors.each { |ancestor|
      if @@options[ancestor] &&
        @@options[ancestor][keyword]
        if (getter = @@options[ancestor][keyword][:getter])
          who.send(:define_method, attribute) {
            getter.call(self, attribute)
          }
          getter_found = true
        end
        if @@options[ancestor][keyword][:getter] === false
          getter_found = true
        end
        if @@options[ancestor][keyword][:setter] === false
          setter_found = true
        end
        if (setter = @@options[ancestor][keyword][:setter])
          who.send(:define_method, attribute.to_s+'=') { |v|
            setter.call(self, attribute, v)
          }
          setter_found = true
        end
        if (callback = @@options[ancestor][keyword][:on_define])
          callback.call(who, attribute, definition)
        end
      end
    }
    if !getter_found
      who.send(:attr_reader, attribute)
    end
    if !setter_found
      who.send(:attr_writer, attribute)
    end

    @@keywords[who][keyword][attribute] = definition
  end

  def self.keywords who
    kwords = {}
    who.ancestors.reverse.each { |subject|
      if info = @@keywords[subject]
        kwords.merge! info.keys
      end
    }
    kwords.keys
  end

  def self.properties who, keyword = nil
    properties = {}
    who.ancestors.reverse.each { |subject|
      if info = @@keywords[subject]
        if keyword
          properties.merge!(info[keyword.to_sym] || {})
        else
          info.each {|k, p| properties.merge! p }
        end
      end
    }
    properties
  end

  def self.definition who, property
    (properties who, nil)[property.to_sym]
  end

  def self.property_names who, keyword = nil
    properties(who, keyword).map { |p| p[0] }
  end

  def self.data who, keyword = nil
    data = {}
    (property_names who.class, keyword).each { |property|
      data[property] = who.send(property)
    }
    data
  end

  def self.set_data object, data, keyword = nil
    (property_names object.class, keyword).each { |property|
      if value = (data[property.to_sym] || data[property.to_s])
        object.send(:instance_variable_set, '@'+property.to_s, value)
      end
    }
  end


   class DefinitionTest < FwTest

     DARTH_VADER = 'Anakin Skywalker'

     def test_enabling_property_inserts_class_with_the_keyword
       pony = Class.new() {
         Pattern::Property.enable(self, :belongs_to)
       }
       assert pony.methods.include? 'belongs_to'
     end

     def test_get_defined_keywords
       son = Class.new() {
         Pattern::Property.enable(self, :belongs_to)
         belongs_to :mama
         belongs_to :papa
       }
       assert_equal Pattern::Property.keywords(son), [:belongs_to]
     end

     def test_get_properties_of_keword
       luke = Class.new() {
         Pattern::Property.enable(self, :parent)
         parent :mama, :name => 'Padme'
         parent :papa, :name => 'Anakin'
       }
       assert_equal Pattern::Property.properties(luke, :parent), {
          :mama => [{:name => 'Padme'}],
          :papa =>  [{:name => 'Anakin'}]
         }
     end

     def test_get_property_definition
       luke = Class.new() {
         Pattern::Property.enable(self, :parent)
         parent :mama, :name => 'Padme'
         parent :papa, :name => 'Anakin'
       }
       eq Pattern::Property.definition(luke, :mama),
          [{:name => 'Padme'}]

       eq Pattern::Property.definition(luke, "mama"),
          [{:name => 'Padme'}]
     end


     def test_get_properties_when_some_keywords_are_not_used
       no_legger = Class.new() {
         Pattern::Property.enable(self, :hand)
         Pattern::Property.enable(self, :leg)
         hand :left
         hand :right
         #no legs
       }
       assert_equal Pattern::Property.properties(no_legger), {
          :left => [], :right =>  [] }
     end

     def test_get_properties_by_keyword_when_some_keywords_are_not_used
       no_legger = Class.new() {
         Pattern::Property.enable(self, :hand)
         Pattern::Property.enable(self, :leg)
         hand :left
         hand :right
         #no legs
       }
       assert_equal Pattern::Property.properties(no_legger, :leg), { }
     end

     def test_properties_generate_accessors
       luke = Class.new() {
         Pattern::Property.enable(self, :parent)
         parent :mother
         parent :father
       }
       fourth_episode_luke = luke.new
       assert_equal fourth_episode_luke.father, nil

       fifth_episode_luke = fourth_episode_luke.clone
       fifth_episode_luke.father = DARTH_VADER

       assert_equal fifth_episode_luke.father, DARTH_VADER
     end

     def test_data_return_hash_with_values_and_properties
       square = Class.new() {
         Pattern::Property.enable(self, :property)
         property :color
         property :side
       }
       s = square.new
       s.color = :red
       s.side = 5
       assert_equal Pattern::Property.data(s, :property), {
         :color => :red, :side => 5
       }
     end

     def test_getter_wrapping
       lazy_class = Class.new() {
         Pattern::Property.enable(self, :lazy, {
           :getter => lambda { |o, attr|
                o.load_lazy attr
              }
           })

         lazy :very_long_text
         lazy :very_big_picture

         def load_lazy attr
           self.send(attr.to_s+'=', { :very_long_text => 'abc' }[attr])
         end
       }
       eq lazy_class.new.very_long_text, 'abc'
     end

     def test_getter_wrapping_gets_inherited
       lazy_class = Class.new() {
         Pattern::Property.enable(self, :lazy, {
           :getter => lambda { |o, attr|
                o.lazy_loaded = attr
              }
           })

         lazy :picture1
         attr_accessor :lazy_loaded
       }

       lazy_child = Class.new(lazy_class) {
         lazy :picture2
       }

       child = lazy_child.new
       child.picture1
       eq child.lazy_loaded, :picture1, "parent getter was not lazy"

       child = lazy_child.new
       child.picture2
       eq child.lazy_loaded, :picture2, "child getter was not lazy"
     end
   end

   class InheritanceTest < FwTest

     def test_get_inherited_keywords
        person = Class.new() {
         Pattern::Property.enable(self, :leg)
         leg :left, :flesh
         leg :right, :flesh
       }
       pirate = Class.new(person)
       assert_equal Pattern::Property.keywords(pirate), [:leg]
     end

     def test_get_inherited_and_own_keywords
        person = Class.new() {
         Pattern::Property.enable(self, :leg)
         leg :left, :flesh
         leg :right, :flesh
       }
       pirate = Class.new(person) {
         Pattern::Property.enable(self, :arm)
         arm :right, :contains => 'parrot'
       }
       assert_equal Pattern::Property.keywords(pirate), [:leg, :arm]
     end

     def test_get_properties_returns_parent_properties_as_well
        person = Class.new() {
         Pattern::Property.enable(self, :leg)
         leg :left, :flesh
         leg :right, :flesh
       }
       pirate = Class.new(person)
       assert_equal Pattern::Property.properties(pirate, :leg),
         {:left => [:flesh], :right => [:flesh]}
     end

     def test_get_properties_returns_parent_properties_overriden_with_our_properties
        person = Class.new() {
         Pattern::Property.enable(self, :leg)
         leg :left, :flesh
         leg :right, :flesh
       }
       old_pirate = Class.new(person) {
         leg :left, :peg
       }
       assert_equal Pattern::Property.properties(old_pirate, :leg),
         {:left => [:peg], :right => [:flesh]}

       assert_equal Pattern::Property.properties(person, :leg),
         {:left => [:flesh], :right => [:flesh]}
     end

     def test_property_names_returns_all_property_names_if_keyword_not_given
        person = Class.new() {
         Pattern::Property.enable(self, :leg)
         Pattern::Property.enable(self, :hand)
         leg :left_leg
         leg :right_leg
         hand :left_hand
         hand :right_hand
       }

       eq Pattern::Property.property_names(person), [
           :left_leg, :right_leg, :left_hand, :right_hand
         ]
     end
   end

   class DataTest < FwTest
     @@person ||= Class.new() {
         Pattern::Property.enable(self, :leg)
         leg :left_leg
         leg :right_leg
       }

     def test_set_data
       p = @@person.new
       Pattern::Property.set_data p, {
           "left_leg" => :peg,
           :right_leg => :flesh
         }

       eq p.left_leg, :peg
       eq p.right_leg, :flesh
     end

     def test_set_data_with_stringified_keys
       p = @@person.new
       Pattern::Property.set_data p, {
           'left_leg' => :peg,
           :right_leg => :flesh
         }

       eq p.left_leg, :peg
       eq p.right_leg, :flesh
     end

     def test_get_data
       p = @@person.new
       p.left_leg = :peg
       p.right_leg = :flesh

       eq Pattern::Property.data(p), {
           :left_leg => :peg,
           :right_leg => :flesh
         }

     end
   end
 end

end

